<script setup lang="ts">
import type { UploadProps, UploadUserFile } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import gifshot from 'gifshot'
import BigNumber from 'bignumber.js'

// 配置中文语言包
let locale = ref(zhCn)

// 暗色模式
const isDark = useDark({
  selector: 'html',
  attribute: 'class',
  valueDark: 'dark',
  valueLight: 'light'
})
useToggle(isDark)

function findInMap<T extends any>(
  value: T,
  mappingTable: {
    [index: string]: (value: T) => T
  },
  compare: (
    pre: {
      key: string
      value: T
    },
    cur: {
      key: string
      value: T
    }
  ) => {
    key: string
    value: T
  }
) {
  let arr = []
  for (const [k, fn] of Object.entries(mappingTable)) {
    arr.push({ key: k, value: fn(value) })
  }
  return arr.reduce(compare)
}

function formatByte(byte: number, formatString?: string) {
  formatString ??= '{output} {units}'
  let { key: units, value: output } = findInMap(
    byte,
    {
      B: (byte: number) => byte,
      KB: (byte: number) => byte / 1e3,
      MB: (byte: number) => byte / 1e6,
      GB: (byte: number) => byte / 1e9,
      TB: (byte: number) => byte / 1e12,
      PB: (byte: number) => byte / 1e15,
      EB: (byte: number) => byte / 1e18,
      ZB: (byte: number) => byte / 1e21,
      YB: (byte: number) => byte / 1e24
    },
    (pre, cur) => {
      if (new BigNumber(cur.value).toFixed().length < new BigNumber(pre.value).toFixed().length)
        return cur
      else return pre
    }
  )
  return formatString
    .replaceAll('{output}', new BigNumber(output).toFixed())
    .replaceAll('{units}', units)
}

// 图片预览
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
  dialogImageUrl.value = uploadFile.url!
  dialogVisible.value = true
}
// 图片移除
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
  uploadFile
  uploadFiles
}

// 数组拼接
function* nestArray<T extends any>(...arr: T[][]): IterableIterator<T[]> {
  if (arr.length === 1) for (const i of arr[0]) yield [i]
  else {
    let tempArr: T[][] = []
    for (const i of arr[0]) {
      for (const j of nestArray(...arr.slice(1))) {
        tempArr.push(j)
        yield [i, ...j]
      }
      break
    }
    for (const i of arr[0].slice(1)) for (const j of tempArr) yield [i, ...j]
  }
}

// Base64 转换为 Blob
function base64ToBlob(base64: string) {
  let [head, body] = base64.split(',')
  // 去掉url的头，并转化为byte
  let bytes = window.atob(body)
  let length = bytes.length
  // 处理异常,将ascii码小于0的转换为大于0
  let ab = new ArrayBuffer(length)
  // 生成视图（直接针对内存）：8位无符号整数，长度1个字节
  let ia = new Uint8Array(ab)
  for (let i = 0; i < length; i++) {
    ia[i] = bytes.charCodeAt(i)
  }
  return new Blob([ab], {
    type: head.match(/:(.*?);/)?.[1]
  })
}

// 下载文件
function download(src: string, fileName: string) {
  const ele = document.createElement('a')
  ele.href = src
  ele.download = fileName
  ele.click()
}

// 表单数据
const form = reactive({
  file: ref<UploadUserFile[]>([]),
  row: 2,
  col: 2,
  frameDuration: 1,
  sampleInterval: 2,
  numWorkers: 5
})

let isStart = ref(false)
let indeterminate = ref<{ uid: number; indeterminate: boolean }[]>([])
const canvasNode = ref(null)
const progress = ref<{ uid: number; percentage: number }[]>([])

// 表单提交事件
const onSubmit = () => {
  isStart.value = true
  let arr = []
  for (const e of form.file) {
    arr.push(
      new Promise((resolve, reject) => {
        indeterminate.value.push({ uid: e.uid as number, indeterminate: true })
        progress.value.push({ uid: e.uid as number, percentage: 0 })
        let img = new Image()
        img.addEventListener('load', async () => {
          // 获取图片宽高
          let width = img.naturalWidth
          let height = img.naturalHeight
          await nextTick()
          // 获取 canvas 对象
          let canvas: HTMLCanvasElement = (canvasNode.value as unknown as HTMLCanvasElement[]).find(
            (canvas) => canvas.dataset.uid == e.uid
          ) as unknown as HTMLCanvasElement
          canvas.width = width
          canvas.height = height
          await nextTick()
          let ctx = canvas.getContext('2d') as CanvasRenderingContext2D
          // 计算行、列
          let col = form.col
          let row = form.row
          let colWidth = width / col
          let rowHeight = height / row
          let ouputBase64s: string[] = []
          for (const [x, y] of nestArray(
            Array(col)
              .fill(null)
              .map((_, i) => i * colWidth),
            Array(row)
              .fill(null)
              .map((_, i) => i * rowHeight)
          )) {
            ctx.drawImage(img, 0, 0)
            ctx.fillRect(x, y, colWidth, rowHeight)
            // ctx.drawImage(img, 0, 0)
            // ctx.globalCompositeOperation = 'destination-in'
            // ctx.fillRect(x, y, colWidth, rowHeight)
            // ctx.globalCompositeOperation = 'destination-over'
            // ctx.fillRect(x, y, width, height)
            // ctx.globalCompositeOperation = 'source-over'
            ouputBase64s.push(canvas.toDataURL('image/png', 1))
          }
          ctx.drawImage(img, 0, 0)
          ;(
            indeterminate.value.find((indeterminate) => indeterminate.uid == e.uid) as unknown as {
              uid: number
              indeterminate: boolean
            }
          ).indeterminate = false
          gifshot.createGIF(
            {
              images: ouputBase64s,
              gifWidth: width,
              gifHeight: height,
              frameDuration: form.frameDuration,
              sampleInterval: form.sampleInterval,
              numWorkers: form.numWorkers,
              progressCallback(percentage: number) {
                ;(
                  progress.value.find((progress) => progress.uid == e.uid) as unknown as {
                    uid: number
                    percentage: number
                  }
                ).percentage = percentage ?? 0 * 100
              }
            },
            (obj: {
              image: string
              cameraStream: any
              error: boolean
              errorCode: number
              errorMsg: string
              savedRenderingContexts: any
            }) => {
              if (obj.error) {
                reject(obj.errorMsg)
              } else {
                let url = URL.createObjectURL(base64ToBlob(obj.image))
                download(url, `${e.name} - ${e.uid}.gif`)
                resolve(url)
              }
            }
          )
        })
        img.src = e.url as string
      })
    )
  }
  Promise.allSettled(arr).finally(() => {
    isStart.value = false
  })
}
</script>

<template>
  <el-config-provider :locale="locale">
    <el-container>
      <el-header>
        <h1>图片防截图</h1>
      </el-header>
      <el-main>
        <el-form :model="form" label-width="80px">
          <el-form-item label="选择文件">
            <el-upload
              v-model:file-list="form.file"
              action="#"
              list-type="picture-card"
              :multiple="true"
              :drag="true"
              accept="image/*"
              :on-preview="handlePictureCardPreview"
              :on-remove="handleRemove"
              :auto-upload="false"
            >
              <el-icon><Plus /></el-icon>
            </el-upload>
            <el-dialog v-model="dialogVisible">
              <el-image
                :src="dialogImageUrl"
                :zoom-rate="1.2"
                :hide-on-click-modal="true"
                :preview-src-list="form.file.map((e) => e.url)"
                :initial-index="form.file.findIndex((e) => e.url === dialogImageUrl)"
                fit="contain"
                class="imagePreview"
              />
            </el-dialog>
          </el-form-item>
          <el-form-item label="图片分块">
            <el-row style="gap: 10px">
              <el-col :span="2" class="text-center">
                <span class="text-gray-500">行</span>
              </el-col>
              <el-col :span="4">
                <el-input-number v-model="form.row" :min="1" />
              </el-col>
            </el-row>
            <el-row style="width: 20px"></el-row>
            <el-row style="gap: 10px">
              <el-col :span="2" class="text-center">
                <span class="text-gray-500">列</span>
              </el-col>
              <el-col :span="4">
                <el-input-number v-model="form.col" :min="1" />
              </el-col>
            </el-row>
          </el-form-item>
          <el-form-item label="图片质量">
            <el-popover
              placement="bottom"
              title="图片质量"
              :width="200"
              trigger="click"
              content="数值越低，质量越好，时间越长"
            >
              <template #reference>
                <el-input-number v-model="form.sampleInterval" :min="1" />
              </template>
            </el-popover>
          </el-form-item>
          <el-form-item label="停留时间">
            <el-popover
              placement="bottom"
              title="停留时间"
              :width="200"
              trigger="click"
              content="每帧的停留时长 10 = 1s"
            >
              <template #reference>
                <el-input-number v-model="form.frameDuration" :min="1" />
              </template>
            </el-popover>
          </el-form-item>
          <el-form-item label="线程数">
            <el-popover
              placement="bottom"
              title="线程数"
              :width="200"
              trigger="click"
              content="使用的 Web Workers 数量，数量越多，速度越快"
            >
              <template #reference>
                <el-input-number v-model="form.numWorkers" :min="1" />
              </template>
            </el-popover>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="onSubmit">开始</el-button>
          </el-form-item>
        </el-form>
        <template v-if="isStart">
          <el-divider />
          <ul>
            <template v-for="item in form.file" :key="item.uid">
              <li class="todoList">
                <div class="canvas">
                  <canvas ref="canvasNode" :data-uid="item.uid"></canvas>
                </div>
                <div class="fileName">
                  <div class="header">文件名</div>
                  <div class="main">{{ item.name }}</div>
                </div>
                <div class="fileSize">
                  <div class="header">文件大小</div>
                  <div class="main">
                    <span>{{ formatByte(item.size as number) }}</span>
                  </div>
                </div>
                <div class="progress">
                  <el-progress
                    :percentage="
                      (progress.find(({ uid }) => uid == item.uid)?.percentage ?? 0) * 100
                    "
                    :indeterminate="
                      indeterminate.find(({ uid }) => uid == item.uid)?.indeterminate ?? false
                    "
                    :format="(percentage:number)=>percentage.toFixed(2)+' %'"
                    ref="progressNode"
                    :data-uid="item.uid"
                  />
                </div>
              </li>
            </template>
          </ul>
        </template>
      </el-main>
    </el-container>
  </el-config-provider>
</template>

<style lang="less" scoped>
// dialog 图片居中
:deep(.el-dialog__body) {
  display: flex;
  justify-content: center;
}
.todoList {
  display: flex;
  align-items: center;
  gap: 20px;
  // canvas
  .canvas {
    width: 100px;
    height: 100px;
    canvas {
      object-fit: contain;
      width: 100%;
      height: 100%;
    }
  }
  // 图片信息
  .fileSize,
  .fileName {
    flex: 1;
    width: 0;
  }
  // 溢出省略号
  .fileSize,
  .fileName {
    .main {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
  // 进度条
  .progress {
    flex: 3;
  }
}
</style>
